#! /usr/bin/env python

# Music of Life, or Music's Brain.
# Copyright(c) 2012 Evan Kline
#
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
#to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
#and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
#IN THE SOFTWARE.
#
#Various parts (base cellular automata components) come from: http://www.zoklet.net/bbs/showthread.php?t=237462
# Conway's game of life
# Copyright(c) 2012 Lan "Lanny" Rogers

import pygame, time, fluidsynth, struct, gzip
from pygame.locals import *

from basicgui import *


DEAD = 0
ALIVE = 1
DYING = 2
NOTE = 3

class lifeGrid() :
	def __init__(self, width, height, brians=False, game='conway'):
		self.wh = (width, height)
		if game == 'brian':
			self.brians = True
		if brians:
			self.brians = True
		
		self.resetGrid()
	
	def resetGrid(self):
		# Populate our grid
		self.grid = []
		width, height = self.wh
		for col in range(width) :
			self.grid.append([])

			for cell in range(height) :
				self.grid[-1].append(DEAD)

	def set(self, value, x, y) :
		'''Sets cell x, y to value'''
		self.grid[x][y] = value

	def toggle(self, x, y) :
		if self.grid[x][y] == ALIVE:
			self.grid[x][y] = DYING
		elif self.grid[x][y] == DYING:
			self.grid[x][y] = DEAD
		else:
			self.grid[x][y] = ALIVE
		return self.grid[x][y]

	def get(self, x, y):
		'''Return cell state at x,y'''
		return self.grid[x][y]

	def birth(self, x, y):
		'''Force cell x,y to life'''
		if self.grid[x][y] == ALIVE:
			return False
		else:
			self.grid[x][y] = ALIVE
			return True

	def kill(self, x, y) :
		'''Force cell x,y to death'''
		if self.grid[x][y] == DEAD:
			return False
		else:
			self.grid[x][y] = DEAD
			return True
	
	def killing(self, x,y):
		'''Force cell x,y to die'''
		if self.grid[x][y] == DYING:
			return False
		else:
			self.grid[x][y] = DYING
			return True

	def get_neighbors(self, x, y) :
		'''Return list of neighbors, live or dead'''
		possibleNeighbors = [[x+1, y],
							 [x+1, y+1],
							 [x+1, y-1],
							 [x-1, y],
							 [x-1, y+1],
							 [x-1, y-1],
							 [x, y+1],
							 [x, y-1]]

		trueNeighbors= []


		for z in possibleNeighbors :
			if z[0] == -1 :
				z[0] = self.get_dims()[0] - 1
			
			elif z[0] == self.get_dims()[0] :
				z[0] = 0

			if z[1] == -1 :
				z[1] = self.get_dims()[1] - 1
			
			elif z[1] == self.get_dims()[1] :
				z[1] = 0


			trueNeighbors.append(self.grid[z[0]][z[1]])

		return trueNeighbors

	def get_live_neighbors(self, x, y) :
		'''Return number of living cells adjacent to cell x,y'''
		live_count = 0
		for neighbor in self.get_neighbors(x, y) :
			if neighbor == ALIVE :
				live_count += 1

		return live_count

	def get_dims(self) :
		return (len(self.grid[0]), len(self.grid))


class Note():
	def __init__(self, midival, basevel, synth, track = 0):
		self.midival = midival
		self.basevel = basevel
		self.synth = synth
		self.track = track
		
	def play(self, addedvel=0):
		self.synth.noteon(self.track, self.midival, self.basevel + addedvel)
	
	def stop(self):
		self.synth.noteoff(self.track, self.midival)

def conways_life(start_grid) :
	x, y = start_grid.get_dims()
	new_grid = lifeGrid(x, y)

	updateRects = []
	for mapy in range(y) :
		for mapx in range(x) :
			if start_grid.get_live_neighbors(mapx, mapy) < 2 or start_grid.get_live_neighbors(mapx, mapy) > 3 :
				if start_grid.get(mapx, mapy) :
					# If we're actually changing the value of the cell, add it
					# to the list of cells to be re-drawn
					updateRects.append(coord_to_px(start_grid, mapx, mapy))

				new_grid.kill(mapx, mapy)

			elif start_grid.get_live_neighbors(mapx, mapy) == 3 :
				if not start_grid.get(mapx, mapy) :
					updateRects.append(coord_to_px(start_grid, mapx, mapy))

				new_grid.birth(mapx, mapy)

			else :
				new_grid.set(start_grid.get(mapx, mapy), mapx, mapy)

	return (new_grid, updateRects)

def brians_brain(start_grid) :
	x, y = start_grid.get_dims()
	new_grid = lifeGrid(x, y, brians=True)

	updateRects = []
	for mapy in range(y) :
		for mapx in range(x) :
			if start_grid.get(mapx, mapy) == DEAD:
				if start_grid.get_live_neighbors(mapx, mapy) == 2:
					updateRects.append(coord_to_px(start_grid, mapx, mapy))
					new_grid.birth(mapx, mapy)
				else:
					new_grid.kill(mapx, mapy)
			elif start_grid.get(mapx, mapy) == ALIVE:
				updateRects.append(coord_to_px(start_grid, mapx, mapy))
				new_grid.killing(mapx, mapy)
			elif start_grid.get(mapx, mapy) == DYING:
				updateRects.append(coord_to_px(start_grid, mapx, mapy))
				new_grid.kill(mapx, mapy)

			else:
				new_grid.set(start_grid.get(mapx, mapy), mapx, mapy)

	return (new_grid, updateRects)

def px_to_coord(grid, x, y) :
	gridx = x / 10
	gridy = -((y / 10)-grid.get_dims()[1]+1)

	#print gridx, gridy
	return (gridx, gridy)

def coord_to_px(grid, x, y) :
	mapy = (grid.get_dims()[1] - 1 - y) * 10
	mapx = (x) * 10
	#print mapx, mapy

	return pygame.Rect(mapx, mapy, 9, 9)

def constrain(val, minv, maxv):
	if val > maxv:
		return maxv
	elif val < minv:
		return minv
	else:
		return val

def constraintuple(tup, minv, maxv):
	retlist = []
	for i in tup:
		retlist.append(constrain(i, minv, maxv))
	return tuple(retlist)

def renderGrid(screen, grid, notes, background, colors):
	for gridy in range(grid.get_dims()[1]) :
		mapy = (grid.get_dims()[1] - 1 - gridy) * 10
		for gridx in range(grid.get_dims()[0]) :
			mapx = gridx * 10
			
			if grid.get(gridx, gridy) == DEAD:
				pygame.draw.rect(screen, colors[DEAD], (mapx, mapy, 9, 9))
				
			for nt in notes:
				if (gridx, gridy) == nt[2]:
					if nt != 0:
						coloradd = 30*nt[1]
						color = list(colors[NOTE][nt[0]])
						for i in xrange(3):
							color[i] += coloradd
						color = constraintuple(color, 0, 255)
					pygame.draw.rect(screen, color, (mapx, mapy, 9, 9))
				
				
			if grid.get(gridx, gridy) == ALIVE:
				pygame.draw.rect(screen, colors[ALIVE], (mapx, mapy, 9, 9))
				
			elif grid.get(gridx, gridy) == DYING:
				pygame.draw.rect(screen, colors[DYING], (mapx, mapy, 9, 9))

def renderGui(screen, drawelements, guicolors, generation, font):
	vsep1 = 40
	seperators = [((90,vsep1),(90,GUI_HEIGHT)), ((310,vsep1),(310,GUI_HEIGHT)), ((0,vsep1),(WINDOW_SIZE[0],vsep1))]
	
	pygame.draw.rect(screen, guicolors[0], (0,0,600,GUI_HEIGHT)) #GUI Background
	
	for sep in seperators: #Draw seperators
		pygame.draw.line(screen, guicolors[4], sep[0], sep[1])
		
	for element in drawelements:
		element.draw(screen)
		
	textsurface = font.render("Generation %d" % generation, 1, guicolors[3])
	screen.blit(textsurface, (500, GUI_HEIGHT-20))

def renderScreen():
	screen.blit(background, (0,0))
	renderGrid(screen, grid, Notes, background, COLORS)
	renderGui(guiscreen, drawelements, GUICOLORS, generation, mainfont)
	screen.blit(guiscreen, (0, WINDOW_SIZE[1]))

def quitgame():
	global running
	running = False

def resetgrid():
	global grid, generation
	grid.resetGrid()
	generation = 0

def resetgame():
	resetgrid()
	global Notes
	Notes = []
	status.text = "Reset grid"
	
def savegrid(name):
	if saveNotes.state:
		return savenotes(name)
		
	wgrid = grid.grid
	griddata = ''
	notedata = ''
	f = gzip.open("savefiles/%s.bbmz"%name,'wb')
	
	for x, col in enumerate(wgrid):
		for y, rowitem in enumerate(col):
			if rowitem != DEAD:
				griddata += struct.pack("<iiBxxx", x, y, rowitem)
	
	for n in Notes:
		notedata += struct.pack("<BBiixx", n[0],n[1],n[2][0],n[2][1])
		
	f.write( struct.pack("<ii", len(griddata), len(notedata)) )
	f.write( griddata )
	f.write( notedata )
		
	print("Saved file %s.bbmz sucessfully, with %d notes. Section lengths: %d %d " %(name, len(Notes), len(griddata), len(notedata)))
	status.text = "Saved file %s.bbmz sucessfully." % name

def savenotes(name):
	try:
		f = gzip.open("savefiles/%s.bbmz"%name, 'rb')
		gridlen = struct.unpack("<i", f.read(4))[0]
		griddata = f.read(gridlen)
		f.close()
	except IOError:
		griddata = ''
	
	notedata = ''
	for n in Notes:
		notedata += struct.pack("<BBiixx", n[0],n[1],n[2][0],n[2][1])
		
	f = gzip.open("savefiles/%s.bbmz"%name, 'wb')
	f.write( struct.pack("<ii", len(griddata), len(notedata)) )
	f.write( griddata )
	f.write( notedata )
	f.close()
	print("Saved notes to file %s.bbmz sucessfully, with %d notes. Section lengths: %d %d " %(name, len(Notes), len(griddata), len(notedata)))
	status.text = "Saved notes to file %s.bbmz sucessfully." % name

def unpackdata(griddata, grid):
	while True:
		data = griddata[:struct.calcsize("<iiBxxx")]
		griddata = griddata[struct.calcsize("<iiBxxx"):]
		if len(data) == 0:
			break
		x,y,s = struct.unpack("<iiBxxx", data)
		grid.set(s,x,y)

def loadgrid(name):
	global grid
	global Notes
	
	resetgrid()
	Notes = []
	
	f = gzip.open("savefiles/%s.bbmz"%name,'rb')
	gridlen, notelen = struct.unpack("<ii", f.read(8))
	
	griddata = f.read(gridlen)
	notedata = f.read(notelen)
	
	f.close()
	
	
	while True:
		data = griddata[:struct.calcsize("<iiBxxx")]
		griddata = griddata[struct.calcsize("<iiBxxx"):]
		if len(data) == 0:
			break
		x,y,s = struct.unpack("<iiBxxx", data)
		grid.set(s,x,y)
	
	while True:
		data = notedata[:struct.calcsize("<BBiixx")]
		notedata = notedata[struct.calcsize("<BBiixx"):]
		if len(data) == 0:
			break
		c,n,x,y = struct.unpack("<BBiixx", data)
		Notes.append((c,n,(x,y)))
	
	print("Loaded file %s.bbmz sucessfully, with %d notes" %(name, len(Notes)))
	status.text = "Loaded file %s.bbmz sucessfully." % name

WINDOW_SIZE = (600,600)
GUI_HEIGHT = 150
BACKGROUND_COLOR = (0, 0, 0)
DEAD_COLOR = (50, 50, 50)
DYING_COLOR = (150, 0, 0)
LIVE_COLOR = (255, 255, 0)
GUI_BG = (182,182,196)
GUI_BUTTON_OFF = (47,168,203)
GUI_BUTTON_ON = (0,131,182)
GUI_TEXT = (10,10,10)
GUI_SEP = (0,0,0)

NOTE_COLORS = [(0,0,255), (60,60,255), (108,91,255)]

COLORS = [DEAD_COLOR, LIVE_COLOR, DYING_COLOR, NOTE_COLORS]
GUICOLORS = [GUI_BG, GUI_BUTTON_OFF, GUI_BUTTON_ON, GUI_TEXT, GUI_SEP]

briansbrain = True
running = True


#Synth setup
fs = fluidsynth.Synth()
fs.start()
sfid = fs.sfload("5_rhodes3.sf2")
fs.program_select(0, sfid, 0, 0)

fs.program_select(1, sfid, 0, 0)
fs.program_select(2, sfid, 0, 0)

# Pygame ceremony.
pygame.init()
pygame.display.set_caption("Brian's Brain on Music")
screen = pygame.display.set_mode((WINDOW_SIZE[0],WINDOW_SIZE[1]+GUI_HEIGHT))
background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill(BACKGROUND_COLOR)

guiscreen = pygame.Surface((WINDOW_SIZE[0], GUI_HEIGHT))

grid = lifeGrid(60, 60, brians=briansbrain)
	

updateRects = []

generation = 0

NotesChan1 = [ (Note(48, 70, fs),Note(51, 60, fs)), (Note(55, 70, fs),Note(58, 60, fs)), (Note(60, 70, fs),Note(65, 60, fs)) ]
NotesChan2 = [ (Note(55, 65, fs),Note(58, 57, fs)), (Note(63, 65, fs),Note(65, 57, fs)), (Note(67, 65, fs),Note(72, 57, fs)) ]
NotesChan3 = [ (Note(60, 67, fs),Note(63, 59, fs)), (Note(67, 67, fs),Note(70, 59, fs)), (Note(72, 67, fs),Note(77, 59, fs)) ]

NoteLists = [NotesChan1, NotesChan2, NotesChan3]

Notes = []

##GUI elements:##

#Main font for GUI elements.
mainfont = pygame.font.SysFont("Courier New", 12)

status = StatusTextLine((20,10), pygame.font.SysFont("Courier New", 14), text="Grid initalized")

buttons = OptionSet( GUICOLORS[1:], font=mainfont )
notechanbtns = OptionSet(GUICOLORS[1:], font=mainfont, wh=(15,15) )

runningBtn = BoolButton( (20, 60), (30,30), colors=[(139,107,91),(190,149,129),GUI_TEXT], font=mainfont, title="Stopped", toggletitle="Running")
quitBtn = ActionButton( (20, 110), quitgame, (30,30), colors=[(120,90,80),GUI_TEXT], font=mainfont, title="Quit")

cellBtn = buttons.addButton( (100, 70), title = "Cell", initial=1)
eraseBtn = buttons.addButton( (150, 70), title = "Erase", initial=0, colors=[(224,225,185),(209,195,170),GUI_TEXT])
resetBtn = MultipleClickButton( (200, 70), resetgame, titles=["Reset", "Really?"], clicks=2, colors=[(139,107,91),(190,149,129),GUI_TEXT], font = mainfont)

note1Btn = buttons.addButton( (100, 105), title = "Note 1" )
note2Btn = buttons.addButton( (150, 105), title = "Note 2" )
note3Btn = buttons.addButton( (200, 105), title = "Note 3" )

chan1Btn = notechanbtns.addButton( (260, 65), title = "Chan 1", initial=1 )
chan2Btn = notechanbtns.addButton( (260, 93), title = "Chan 2" )
chan3Btn = notechanbtns.addButton( (260, 121), title = "Chan 3" )

saveName = ActionTextBox((320,70), pygame.font.SysFont("Courier New", 16), savegrid, title = "Save File", font = mainfont, maxlen = 15)
loadName = ActionTextBox((320,110), pygame.font.SysFont("Courier New", 16), loadgrid, title = "Load File", font = mainfont, maxlen = 15)
saveNotes = BoolButton((500,70), wh=(10,10), font=pygame.font.SysFont("Courier New", 10), title = "Save:Everything", toggletitle = "Save:Only Notes")

buttonset = [runningBtn, quitBtn, resetBtn, saveNotes]
optionset = [buttons, notechanbtns]
textboxes = [saveName, loadName]
gui_other = [status]

drawelements = buttonset + optionset + textboxes + gui_other
	
while running:
	events = pygame.event.get()
	for event in events:
		if event.type == pygame.QUIT:
			running = False

		if event.type == MOUSEBUTTONDOWN:
			x,y = event.pos
			if y>WINDOW_SIZE[1]: #Everything in here is GUI operations
				y -= WINDOW_SIZE[1]
				for btn in buttonset:
					btn.testclick((x,y))
					
				for opts in optionset:
					opts.testbuttons((x,y))
					
				for txtbox in textboxes:
					txtbox.testclick((x,y))
			else: #Everything in here is changes depending on the GUI elements
				gx,gy = px_to_coord(grid,x,y)
				if chan1Btn.state:
					chan = 0
				elif chan2Btn.state:
					chan = 1
				elif chan3Btn.state:
					chan = 2
				if note1Btn.state:
					Notes.append( (chan, 0, (gx,gy)) )
				if note2Btn.state:
					Notes.append( (chan, 1, (gx,gy)) )
				if note3Btn.state:
					Notes.append( (chan, 2, (gx,gy)) )
				if cellBtn.state:
					grid.toggle(gx,gy)
				if eraseBtn.state:
					if not grid.kill(gx,gy): #If it didn't get to kill a cell, then make it try to kill a note
						for i in xrange(len(Notes)):
							if (gx,gy) == Notes[i][2]:
								del(Notes[i])
								break
					
		if event.type == KEYDOWN:
			for txtbox in textboxes:
				if txtbox.state:
					txtbox.update([event])

	renderScreen()
	pygame.display.update()
	
	if runningBtn.state:
		oldRects = updateRects
		if not briansbrain:
			grid, updateRects = conways_life(grid)
		else:
			grid, updateRects = brians_brain(grid)

		renderScreen()

		if generation == 0 :
			# On first gen, update the whole damn thing
			pygame.display.update()

		# And finally update everything added to updateRects that needs to be
		# drawn to the screen
		updateRects.extend(oldRects)
		pygame.display.update()
		
		for nlist in NoteLists:
			for i in nlist:
				i[0].stop()
				i[1].stop()
		notestrength = [[[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]], [[0, 0], [0, 0], [0, 0]]] #ugly, text derived from: [[[0]*2]*3]*3
		
		for note in Notes:
			state = grid.get(*note[2])
			if state == ALIVE:
				notestrength[note[0]][note[1]][0] += 1
			elif state == DYING:
				notestrength[note[0]][note[1]][1] += 1
		
		for chn in range(len(notestrength)):
			for note in range(len(notestrength[chn])):
				if notestrength[chn][note][0]:
					NoteLists[chn][note][0].play( notestrength[chn][note][0] )
				if notestrength[chn][note][1]:
					NoteLists[chn][note][1].play( notestrength[chn][note][1] )
		time.sleep(.2)

		generation += 1